﻿using System;
using System.Diagnostics;
using System.IO;

namespace SfsTools.Core
{
    /// <summary>
    /// Represents the index area.
    /// </summary>
    internal class IndexArea : Object, IFsComponent, IDisposable
    {
        #region Properties

        /// <summary>
        /// Contains the volume.
        /// </summary>
        private readonly IVolume FVolume;
        /// <summary>
        /// Contains the data area.
        /// </summary>
        private readonly DataArea FDataArea;
        /// <summary>
        /// Contains the start of the index area.
        /// </summary>
        private readonly Int64 FPosition;
        /// <summary>
        /// Contains the entries.
        /// </summary>
        private IndexEntry[] FEntries;
        /// <summary>
        /// Contains the block size.
        /// </summary>
        private int FBlockSize;
        /// <summary>
        /// Contains the map about the free/used index area entries.
        /// </summary>
        private IndexAreaEntryRegionMap FIndexMap;

        /// <summary>
        /// Gets the count of entries in this index area.
        /// </summary>
        public Int32 Length
        {
            [DebuggerStepThrough]
            get
            {
                return FEntries.Length;
            }
        }

        /// <summary>
        /// Gets the <see cref="IndexEntry"/> specified by <paramref name="index"/>.
        /// </summary>
        /// <param name="index">The zero-based index of the <see cref="IndexEntry"/> to return.</param>
        /// <returns>The <see cref="IndexEntry"/> instance specified by <paramref name="index"/>.</returns>
        public IndexEntry this[int index]
        {
            [DebuggerStepThrough]
            get
            {
                if ((index < 0) || (index >= FEntries.Length))
                    throw new ArgumentOutOfRangeException("index");
                return FEntries[index];
            }
        }

        #endregion

        #region Methods

        #region Misc

        /// <summary>
        /// Reads the index area entries.
        /// </summary>
        /// <param name="volume">The volume to read from.</param>
        /// <param name="factory">The <see cref="IndexEntry"/> factory.</param>
        [DebuggerStepThrough]
        public void ReadEntries(IVolume volume, IndexEntryFactory factory)
        {
            // Read entries.
            for (int i = 0; i < FEntries.Length; i++)
            {
                byte[] buffer = volume.Read(FPosition + i * 64, 64);
                FEntries[i] = factory.GetEntryFor(FPosition + i * 64, buffer);
            }
            // Scan for free entries and build RLE map.
            RebuildIndexRleMap();
            // Create RLE map of free and used data blocks.
            //RebuildDataRleMap();
        }

        /// <summary>
        /// Clears the entry specified by <paramref name="index"/>, marking it unused.
        /// </summary>
        /// <param name="index">The zero-based index of the <see cref="IndexEntry"/> to remove.</param>
        [DebuggerStepThrough]
        public void ClearEntry(int index)
        {
            if ((index < 0) || (index >= FEntries.Length))
                throw new ArgumentOutOfRangeException("index");
            FEntries[index] = new UnusedIndexEntry(FPosition + index * 64, new byte[64]);
            //FEntries[index] = null;
            // Do not mark entry as clear in index map, as this method is
            // called from the index area validator, before the RLE map is created.
        }

        /// <summary>
        /// Sets the block size to <paramref name="value"/>.
        /// </summary>
        /// <param name="value">The size of a block, in bytes.</param>
        [DebuggerStepThrough]
        public void SetBlockSize(int value)
        {
            FBlockSize = value;
        }   

        /// <summary>
        /// Gets an array containing the current entries.
        /// </summary>
        /// <returns>An array containing the current entries.</returns>
        /// <remarks>For testing only.</remarks>
        [DebuggerStepThrough]
        internal IndexEntry[] GetEntries()
        {
            return FEntries;
        }

        #endregion

        #region Create directory

        /// <summary>
        /// Allocates entries for a new directory.
        /// </summary>
        /// <param name="name">The full path to the directory.</param>
        /// <returns>The zero-based index of the index area entry.</returns>
        /// <exception cref="ArgumentNullException">The argument <paramref name="name"/> is <see langword="null"/>.</exception>
        /// <exception cref="IndexAreaFullException">Cannot allocate entries because the index area is full.</exception>
        /// <exception cref="System.IO.PathTooLongException">The full path is too long.</exception>
        [DebuggerStepThrough]
        public int AllocateDirectory(String name)
        {
            return AllocateDirectory(name, false);
        }

        /// <summary>
        /// Allocates entries for a new directory.
        /// </summary>
        /// <param name="name">The full path to the directory.</param>
        /// <param name="deleted">Controls whether to allocate entries for a present or a deleted directory.</param>
        /// <returns>The zero-based index of the index area entry.</returns>
        /// <exception cref="ArgumentNullException">The argument <paramref name="name"/> is <see langword="null"/>.</exception>
        /// <exception cref="IndexAreaFullException">Cannot allocate entries because the index area is full.</exception>
        /// <exception cref="System.IO.PathTooLongException">The full path is too long.</exception>
        public int AllocateDirectory(String name, bool deleted)
        {
            if (name == null)
                throw new ArgumentNullException("name");
            // Count how many bytes in name.
            byte[] buffer = System.Text.Encoding.UTF8.GetBytes(name.Replace(Path.DirectorySeparatorChar, '/'));
            if (buffer.Length > 255 * 64 + 54 - 1)
                throw new PathTooLongException();
            // Calculate how many continuation entries are required. Include zero terminator byte.
            int continuationCount = ((buffer.Length - 53) + 63) / 64;
            int entryCount = 1 + continuationCount;
            Debug.Assert(continuationCount <= 255);
            // Find this many IA entries.
            int index = FIndexMap.FindAndAllocateEntries(entryCount);
            // Allocate and initialize entries.
            ContinuationEntry[] contEntries = new ContinuationEntry[continuationCount];
            BaseDirectoryEntry entry = deleted ? (BaseDirectoryEntry)DeletedDirectoryEntry.Create(FPosition + index * 64) : PresentDirectoryEntry.Create(FPosition + index * 64);
            entry.CopyFrom(buffer, 0);
            entry.TimeStamp = TimeStamp.Now;
            entry.ContinuationCount = (byte)continuationCount;
            for (int i = 0; i < continuationCount; i++)
            {
                contEntries[i] = new ContinuationEntry(FPosition + (index + i + 1) * 64, new byte[64]);
                contEntries[i].CopyFrom(buffer, 54 + i * 64);
            }
            // Update in-memory entry list.
            FEntries[index] = entry;
            for (int i = 0; i < continuationCount; i++)
                FEntries[index + i + 1] = contEntries[i];
            // Update volume with new entries.
            entry.WriteTo(FVolume);
            for (int i = 0; i < continuationCount; i++)
                contEntries[i].WriteTo(FVolume);
            return index;
        }

        #endregion

        #region Create file

        /// <summary>
        /// Allocates entries for a new directory.
        /// </summary>
        /// <param name="name">The full path to the directory.</param>
        /// <param name="length">Specifies the maximum length of the file that is guaranteed to be available, or <em>-1</em> for no allocation.</param>
        /// <returns>The zero-based index of the index area entry.</returns>
        /// <exception cref="ArgumentNullException">The argument <paramref name="name"/> is <see langword="null"/>.</exception>
        /// <exception cref="IndexAreaFullException">Cannot allocate entries because the index area is full.</exception>
        /// <exception cref="System.IO.PathTooLongException">The full path is too long.</exception>
        [DebuggerStepThrough]
        public int AllocateFile(String name, Int64 length)
        {
            return AllocateFile(name, false, length);
        }

        /// <summary>
        /// Allocates entries for a new directory.
        /// </summary>
        /// <param name="name">The full path to the directory.</param>
        /// <param name="deleted">Controls whether to allocate entries for a present or a deleted directory.</param>
        /// <param name="length">Specifies the maximum length of the file that is guaranteed to be available, or <em>-1</em> for no allocation.</param>
        /// <returns>The zero-based index of the index area entry.</returns>
        /// <exception cref="ArgumentNullException">The argument <paramref name="name"/> is <see langword="null"/>.</exception>
        /// <exception cref="IndexAreaFullException">Cannot allocate entries because the index area is full.</exception>
        /// <exception cref="System.IO.PathTooLongException">The full path is too long.</exception>
        public int AllocateFile(String name, bool deleted, Int64 length)
        {
            if (name == null)
                throw new ArgumentNullException("name");
            // Count how many bytes in name.
            byte[] buffer = System.Text.Encoding.UTF8.GetBytes(name.Replace(Path.DirectorySeparatorChar, '/'));
            if (buffer.Length > 255 * 64 + 30 - 1)
                throw new PathTooLongException();
            // Calculate how many continuation entries are required. Include zero terminator byte.
            int continuationCount = ((buffer.Length - 30) + 63) / 64;
            int entryCount = 1 + continuationCount;
            Debug.Assert(continuationCount <= 255);
            // Find this many IA entries.
            int index = FIndexMap.FindAndAllocateEntries(entryCount);
            // Find this many DA blocks.
            long firstBlock, lastBlock;
            try
            {
                BlockRegion br = FDataArea.AllocateFile(Math.Max(FBlockSize, length));
                firstBlock = br.Start;
                lastBlock = br.Start + br.Length; // exclusive
            }
            catch
            {
                // Release IA entries allocated.
                BlockRegion br = new BlockRegion(index, continuationCount + 1);
                FIndexMap.DeallocateRegion(br);
                throw;
            }
            // Allocate and initialize entries.
            ContinuationEntry[] contEntries = new ContinuationEntry[continuationCount];
            BaseFileEntry entry = deleted ? (BaseFileEntry)DeletedFileEntry.Create(FPosition + index * 64) : PresentFileEntry.Create(FPosition + index * 64);
            entry.CopyFrom(buffer, 0);
            entry.TimeStamp = TimeStamp.Now;
            entry.ContinuationCount = (byte)continuationCount;
            entry.FirstBlock = firstBlock;
            entry.LastBlock = lastBlock;
            //entry.FileSize = 0;
            for (int i = 0; i < continuationCount; i++)
            {
                contEntries[i] = new ContinuationEntry(FPosition + (index + i + 1) * 64, new byte[64]);
                contEntries[i].CopyFrom(buffer, 54 + i * 64);
            }
            // Update in-memory entry list.
            FEntries[index] = entry;
            for (int i = 0; i < continuationCount; i++)
                FEntries[index + i + 1] = contEntries[i];
            // Update volume with new entries.
            entry.WriteTo(FVolume);
            for (int i = 0; i < continuationCount; i++)
                contEntries[i].WriteTo(FVolume);
            return index;
        }

        #endregion

        #region Rename directory

        /// <summary>
        /// Renames an existing directory, possibly allocating new entries for it.
        /// </summary>
        /// <param name="name">The full path to the new directory.</param>
        /// <param name="index">The zero-based index of the index area entry for the directory.</param>
        /// <returns>The zero-based index of the index area entry.</returns>
        /// <exception cref="ArgumentNullException">The argument <paramref name="name"/> is <see langword="null"/>.</exception>
        /// <exception cref="IndexAreaFullException">Cannot allocate entries because the index area is full.</exception>
        /// <exception cref="System.IO.PathTooLongException">The full path is too long.</exception>
        [DebuggerStepThrough]
        public int RenameDirectory(int index, String name)
        {
            return RenameDirectory(index, name, false);
        }

        /// <summary>
        /// Renames an existing directory, possibly allocating new entries for it.
        /// </summary>
        /// <param name="name">The full path to the new directory.</param>
        /// <param name="index">The zero-based index of the index area entry for the directory.</param>
        /// <param name="deleted">Controls whether to allocate entries for a present or a deleted directory.</param>
        /// <returns>The zero-based index of the index area entry.</returns>
        /// <exception cref="ArgumentNullException">The argument <paramref name="name"/> is <see langword="null"/>.</exception>
        /// <exception cref="IndexAreaFullException">Cannot allocate entries because the index area is full.</exception>
        /// <exception cref="System.IO.PathTooLongException">The full path is too long.</exception>
        public int RenameDirectory(int index, String name, bool deleted)
        {
            if (name == null)
                throw new ArgumentNullException("name");
            // Count how many bytes in name.
            byte[] buffer = System.Text.Encoding.UTF8.GetBytes(name.Replace(Path.DirectorySeparatorChar, '/'));
            if (buffer.Length > 255 * 64 + 54 - 1)
                throw new PathTooLongException();
            // Calculate how many continuation entries are required. Include zero terminator byte.
            int continuationCount = ((buffer.Length - 53) + 63) / 64;
            int entryCount = 1 + continuationCount;
            Debug.Assert(continuationCount <= 255);
            // Check how many IA entries this directory currently has.
            BaseDirectoryEntry entry = (BaseDirectoryEntry)FEntries[index];
            ContinuationEntry[] contEntries = new ContinuationEntry[continuationCount];
            if (entry.ContinuationCount == continuationCount)
            {
                // Fast track: no reallocating needed, just rewrite the entries.
                entry.CopyFrom(buffer, 0);
                //entry.TimeStamp = TimeStamp.Now;
                entry.ContinuationCount = (byte)continuationCount;
                for (int i = 0; i < continuationCount; i++)
                {
                    contEntries[i] = FEntries[index + 1 + i] as ContinuationEntry;
                    Debug.Assert(contEntries[i] != null);
                    contEntries[i].CopyFrom(buffer, 54 + i * 64);
                }
            }
            else if (entry.ContinuationCount > continuationCount)
            {
                // Fast track: no reallocation needed, but some continuation entries will be deallocated.
                int oldContCount = entry.ContinuationCount;
                entry.CopyFrom(buffer, 0);
                //entry.TimeStamp = TimeStamp.Now;
                entry.ContinuationCount = (byte)continuationCount;
                for (int i = 0; i < continuationCount; i++)
                {
                    contEntries[i] = FEntries[index + 1 + i] as ContinuationEntry;
                    Debug.Assert(contEntries[i] != null);
                    contEntries[i].CopyFrom(buffer, 54 + i * 64);
                }
                // Mark index entry region occupied by superfluous continuation entries as free.
                // Defer removing them until we absolutely run out of free entries.
                BlockRegion br = new BlockRegion(index + oldContCount, continuationCount - oldContCount);
                FIndexMap.UnRecycleEntries(br);
                // TODO: this work is later duplicated in RecycleEntries, but
                // we can't skip removing the continuation entries. If we skip, and
                // crash, the FS will be in an inconsistent state.
                for (int i = continuationCount; i < oldContCount; i++)
                {
                    UnusedIndexEntry ue = new UnusedIndexEntry(FPosition + (index + i + 1) * 64, FEntries[index + i + 1].GetBuffer());
                    FEntries[index + 1 + i] = ue;
                    ue.WriteTo(FVolume);
                }
                entry.ContinuationCount = (byte)continuationCount;
            }
            else
            {
                // Slow track: reallocation needed.
                int oldContCount = entry.ContinuationCount;
                // TODO: check if entries JUST AFTER entries for this directory are free.
                // Find this many IA entries.
                int index2 = FIndexMap.FindAndAllocateEntries(entryCount);
                BaseDirectoryEntry old = entry;
                // Allocate and initialize entries.
                entry = deleted ? (BaseDirectoryEntry)DeletedDirectoryEntry.Create(FPosition + index2 * 64) : PresentDirectoryEntry.Create(FPosition + index2 * 64);
                entry.CopyFrom(buffer, 0);
                entry.TimeStamp = old.TimeStamp;
                entry.ContinuationCount = (byte)continuationCount;
                for (int i = 0; i < continuationCount; i++)
                {
                    contEntries[i] = new ContinuationEntry(FPosition + (index2 + i + 1) * 64, new byte[64]);
                    contEntries[i].CopyFrom(buffer, 54 + i * 64);
                }
                // Update in-memory entry list.
                FEntries[index2] = entry;
                for (int i = 0; i < continuationCount; i++)
                    FEntries[index2 + i + 1] = contEntries[i];
                // Mark index entry region occupied by deleted entry and continuation entries as free.
                // Defer removing them until we absolutely run out of free entries.
                BlockRegion br = new BlockRegion(index, oldContCount);
                FIndexMap.Recycle(br, new NullRecycleCallback());
                FEntries[index] = new DeletedDirectoryEntry(FPosition + index * 64, FEntries[index].GetBuffer());
                FEntries[index].WriteTo(FVolume);
                // Overwriting the old continuation entries can wait.
                // TODO: this may lead to FS corruption. Imagine the following rename sequence:
                // A -> renamed to B -> renamed to A -> renamed to anything. Now two deleted
                // directory entries for A will be on the volume...
                index = index2;
            }
            // Update volume with new entries.
            entry.WriteTo(FVolume);
            for (int i = 0; i < continuationCount; i++)
                contEntries[i].WriteTo(FVolume);
            return index;
        }

        #endregion

        #region Delete directory

        /// <summary>
        /// Deletes the directory specified by the zero-based <paramref name="index"/> which is an index area entry index.
        /// </summary>
        /// <param name="index">The index area entry index of the directory to delete.</param>
        [DebuggerStepThrough]
        public void DeleteDirectory(int index)
        {
            DeleteDirectory(index, new NullRecycleCallback());
        }

        /// <summary>
        /// Deletes the directory specified by the zero-based <paramref name="index"/> which is an index area entry index.
        /// </summary>
        /// <param name="index">The index area entry index of the directory to delete.</param>
        /// <param name="callback">The callback to invoke when the entry is recycled.</param>
        public void DeleteDirectory(int index, IEntryRecycleCallback callback)
        {
            BaseDirectoryEntry entry = FEntries[index] as BaseDirectoryEntry;
            Debug.Assert(entry != null);
            Debug.Assert(!entry.IsDeleted);
#if false
            DeletedDirectoryEntry dde = new DeletedDirectoryEntry((PresentDirectoryEntry)entry);
            FEntries[index] = dde;
            // Mark index entry region occupied by directory and continuation entries as free.
            // Defer removing them until we absolutely run out of free entries.
            BlockRegion br = new BlockRegion(index, entry.ContinuationCount + 1);
            FIndexMap.Recycle(br, callback);
            // Update volume with new entry.
            dde.WriteTo(FVolume);
#else
            // Mark index entry region occupied by directory and continuation entries as free.
            // Delete them immediately (support for delayed deletion and resurrection is axed from V1)
            BlockRegion br = new BlockRegion(index, entry.ContinuationCount + 1);
            FIndexMap.DeallocateRegion(br);
            // Update volume with new entries.
            for (int i = 0; i < entry.ContinuationCount; i++)
            {
                FEntries[index] = new UnusedIndexEntry(FPosition + (index + i + 1) * 64,
                    FEntries[index].GetBuffer());
                FEntries[index].WriteTo(FVolume);
            }
            FEntries[index] = new UnusedIndexEntry(FPosition + index * 64, entry.GetBuffer());
            FEntries[index].WriteTo(FVolume);
#endif
        }

        #endregion

        #region Delete file

        /// <summary>
        /// Deletes the file specified by the zero-based <paramref name="index"/> which is an index area entry index.
        /// </summary>
        /// <param name="index">The index area entry index of the file to delete.</param>
        [DebuggerStepThrough]
        public void DeleteFile(int index)
        {
            DeleteFile(index, new NullRecycleCallback());
        }

        /// <summary>
        /// Deletes the file specified by the zero-based <paramref name="index"/> which is an index area entry index.
        /// </summary>
        /// <param name="index">The index area entry index of the file to delete.</param>
        /// <param name="callback">The callback to invoke when the entry is recycled.</param>
        public void DeleteFile(int index, IEntryRecycleCallback callback)
        {
            BaseFileEntry entry = FEntries[index] as BaseFileEntry;
            Debug.Assert(entry != null);
            Debug.Assert(!entry.IsDeleted);
#if false
            DeletedFileEntry dfe = new DeletedFileEntry((PresentFileEntry)entry);
            FEntries[index] = dfe;
            // Mark index entry region occupied by directory and continuation entries as free.
            // Defer removing them until we absolutely run out of free entries.
            BlockRegion br = new BlockRegion(index, entry.ContinuationCount + 1);
            FIndexMap.Recycle(br, callback);
            // Update volume with new entry.
            dfe.WriteTo(FVolume);
            // Mark data region allocated by file as free.
            // Defer removing them until we absolutely run out of free space.
            FDataArea.Recycle(new BlockRegion(entry.FirstBlock, entry.LastBlock - entry.FirstBlock)), new ?); // exclusive
#else            
            // Mark index entry region occupied by directory and continuation entries as free.
            // Delete them immediately (support for delayed deletion and resurrection is axed from V1)
            BlockRegion br = new BlockRegion(index, entry.ContinuationCount + 1);
            FIndexMap.DeallocateRegion(br);
            // Update volume with new entries.
            for (int i = 0; i < entry.ContinuationCount; i++)
            {
                FEntries[index] = new UnusedIndexEntry(FPosition + (index + i + 1) * 64,
                    FEntries[index].GetBuffer());
                FEntries[index].WriteTo(FVolume);
            }
            FEntries[index] = new UnusedIndexEntry(FPosition + index * 64, entry.GetBuffer());
            FEntries[index].WriteTo(FVolume);
            // Mark block region allocated by file as free.
            FDataArea.DeleteFile(new BlockRegion(entry.FirstBlock, entry.LastBlock - entry.FirstBlock)); // exclusive
#endif
        }

        #endregion

        #region Resurrect directory

        /// <summary>
        /// Resurrects the directory specified by the zero-based <paramref name="index"/> which is an index area entry index.
        /// </summary>
        /// <param name="index">The index area entry index of the directory to resurrect.</param>
        /// <remarks>Obviously this method cannot be used if the entries for the directory have been overwritten (recycle callback called).</remarks>
        public void ResurrectDirectory(int index)
        {
            throw new NotImplementedException();
#if false
            BaseDirectoryEntry entry = FEntries[index] as BaseDirectoryEntry;
            Debug.Assert(entry != null);
            Debug.Assert(entry.IsDeleted);
            PresentDirectoryEntry dde = new PresentDirectoryEntry((DeletedDirectoryEntry)entry);
            FEntries[index] = dde;
            // Mark index entry region occupied by directory and continuation entries as used.
            BlockRegion br = new BlockRegion(index, entry.ContinuationCount + 1);
            FIndexMap.UnRecycleEntries(br);
            // Update volume with new entry.
            dde.WriteTo(FVolume);
#endif
        }

        #endregion

        #region Compact

        /// <summary>
        /// Compacts the index area.
        /// </summary>
        /// <param name="callback">The entry relocation callback.</param>
        [DebuggerStepThrough]
        public void Compact(IEntryMoveCallback callback)
        {
            if (callback == null)
                callback = new NullMoveCallback();
            FIndexMap.Compact(callback);
        }

        #endregion

        #region Create index RLE map

        #region Misc

        /// <summary>
        /// Returns a value indicating whether the specified index area entry is free.
        /// </summary>
        /// <param name="index">The zero-based index of the index area entry.</param>
        /// <returns></returns>
        [DebuggerStepThrough]
        private bool IsFree(int index)
        {
            if (FEntries[index] == null)
                return true;
            IndexEntry entry = FEntries[index];
            switch (entry.Type)
            {
                case (int)IndexAreaEntryType.Unknown:
                    return true;
                case (int)IndexAreaEntryType.VolumeIdentifier:
                    return false;
                case (int)IndexAreaEntryType.StartingMarker:
                    return false;
                case 0x03:
                case 0x04:
                case 0x05:
                case 0x06:
                case 0x07:
                case 0x08:
                case 0x09:
                case 0x0A:
                case 0x0B:
                case 0x0C:
                case 0x0D:
                case 0x0E:
                case 0x0F:
                    return true;
                case (int)IndexAreaEntryType.Unused:
                    return true;
                case (int)IndexAreaEntryType.PresentDirectory:
                    return false;
                case (int)IndexAreaEntryType.PresentFile:
                    return false;
                case 0x13:
                case 0x14:
                case 0x15:
                case 0x16:
                case 0x17:
                    return true;
                case (int)IndexAreaEntryType.BadSector:
                    return false;
                case (int)IndexAreaEntryType.DeletedDirectory:
                    return false;
                case (int)IndexAreaEntryType.DeletedFile:
                    return false;
                case 0x1B:
                case 0x1C:
                case 0x1D:
                case 0x1E:
                case 0x1F:
                    return true;
                default: // continuation entries
                    return false;
            }
        }

        #endregion

        /// <summary>
        /// Rebuilds the RLE map of free index entries.
        /// </summary>
        [DebuggerStepThrough]
        private void RebuildIndexRleMap()
        {
            uint[] entries = new uint[(FEntries.Length + 31) / 32];
            for (int index = 0; index < FEntries.Length; index++)
            {
                if (IsFree(index))
                {
                    int idx = index / 32;
                    int bit = index % 32;
                    entries[idx] |= (uint)(1 << bit);
                }
            }
            IndexAreaEntryRegionMap map = new IndexAreaEntryRegionMap(entries, FEntries.Length);
            FIndexMap = map;
        }

        #endregion

        #region Set file length

        /// <summary>
        /// Sets the length of the file to <paramref name="length"/>.
        /// </summary>
        /// <param name="index">The zero-based index of the index area entry for the file.</param>
        /// <param name="length">The desired length.</param>
        /// <exception cref="DataAreaFullException">The file cannot be expanded.</exception>
        public void SetFileLength(int index, long length)
        {
            BaseFileEntry entry = FEntries[index] as BaseFileEntry;
            Debug.Assert(entry != null);
            long allocated = (entry.LastBlock - entry.FirstBlock) * FBlockSize; // exclusive
            if (length < entry.FileSize)
            {
                // Shrinking file.
                entry.FileSize = length;
                // TODO: update block allocation map.
            }
            else if (length <= allocated)
            {
                // File has grown, but no blocks need to be allocated.
                entry.FileSize = length;
            }
            else
            {
                // File has grown to the point when new blocks need to be allocated past the end of the file.
                long nextBlock = entry.LastBlock; // exclusive
                long blockCount = (length - allocated - 1) / FBlockSize;
                BlockRegion br = new BlockRegion(nextBlock, blockCount);
                FDataArea.AllocateRegion(br);
                entry.LastBlock = br.Start + br.Length; // exclusive
                entry.FileSize = length;
            }
            // Update entry on disk.
            entry.WriteTo(FVolume);
        }

        #endregion

        #endregion

        #region Constructors
        /// <overloads>Initializes a new instance of <see cref="IndexArea"/>.</overloads>
        /// <summary>
        /// Initializes a new instance of <see cref="IndexArea"/>.
        /// </summary>
        /// <param name="volume">The volume.</param>
        /// <param name="data">The data area.</param>
        /// <param name="position">The start of the index area, in bytes.</param>
        /// <param name="count">The count of entries.</param>
        [DebuggerStepThrough]
        public IndexArea(IVolume volume, DataArea data, Int64 position, Int32 count)
            : base()
        {
            FVolume = volume ?? new NullVolume();
            FDataArea = data;
            FPosition = position;
            FEntries = new IndexEntry[count];
        }
        /// <summary>
        /// Initializes a new instance of <see cref="IndexArea"/>.
        /// </summary>
        /// <param name="volume">The volume.</param>
        /// <param name="data">The data area.</param>
        /// <param name="position">The start of the index area, in bytes.</param>
        /// <param name="array">The array containing the entries.</param>
        /// <exception cref="ArgumentNullException">The argument <paramref name="array"/> is <see langword="null"/>.</exception>
        [DebuggerStepThrough]
        public IndexArea(IVolume volume, DataArea data, Int64 position, IndexEntry[] array)
        {
            if (array == null)
                throw new ArgumentNullException("array");
            FVolume = volume ?? new NullVolume();
            FDataArea = data;
            FPosition = position;
            FEntries = array;
        }
        #endregion

        #region IFsComponent Members

        /// <summary>
        /// Writes the component back to the volume.
        /// </summary>
        /// <param name="volume">The volume.</param>
        [DebuggerStepThrough]
        void IFsComponent.WriteTo(IVolume volume)
        {
            byte[] buffer = new byte[64];
            buffer[0] = 0x10; // unused entry.
            // Write each entry to the volume.
            for (int i = 0; i < FEntries.Length; i++)
            {
                if (FEntries[i] != null)
                {
                    FEntries[i].WriteTo(volume);
                }
                else
                {
                    // Write an unused entry.
                    volume.Write(FPosition + i * 64, buffer);
                }
            }
        }

        #endregion
        
        #region IDisposable Members

        /// <summary>
        /// Releases all resources used by this <see cref="IndexArea"/> instance.
        /// </summary>
        [DebuggerStepThrough]
        public void Dispose()
        {
            FEntries = new IndexEntry[0];
        }

        /// <summary>
        /// Releases all resources used by this <see cref="IndexArea"/> instance.
        /// </summary>
        [DebuggerStepThrough]
        void IDisposable.Dispose()
        {
            FEntries = new IndexEntry[0];
        }

        #endregion

        #region Helper classes

        /// <summary>
        /// Implements a dummy volume.
        /// </summary>
        private class NullVolume : Object, IVolume
        {
            #region Constructors
            /// <overloads>Initializes a new instance of <see cref="NullVolume"/>.</overloads>
            /// <summary>
            /// Initializes a new instance of <see cref="NullVolume"/>.
            /// </summary>
            [DebuggerStepThrough]
            public NullVolume()
                : base()
            {
            }
            #endregion

            #region IVolume Members

            /// <summary>
            /// Gets the length of the volume, in bytes.
            /// </summary>
            /// <exception cref="NotSupportedException">An attempt was made to query the size of the volume on a volume which does not support this operation.</exception>
            long IVolume.Length
            {
                [DebuggerStepThrough]
                get
                {
                    throw new NotSupportedException();
                }
            }

            /// <summary>
            /// Reads <paramref name="count"/> bytes from the volume.
            /// </summary>
            /// <param name="offset">The byte offset to start reading from.</param>
            /// <param name="count">The count of bytes read.</param>
            /// <returns>An array contaning the bytes read.</returns>
            /// <exception cref="EndOfStreamException">There is not enough space to read all the bytes (more bytes requested than the volume contains).</exception>
            /// <exception cref="ArgumentOutOfRangeException">The argument <paramref name="count"/> is out of valid range.</exception>
            [DebuggerStepThrough]
            byte[] IVolume.Read(long offset, int count)
            {
                return new byte[count];
            }

            /// <summary>
            /// Writes the contents of <paramref name="buffer"/> to the volume.
            /// </summary>
            /// <param name="offset">The byte offset to start writing at.</param>
            /// <param name="buffer">The array containing the bytes to write.</param>
            /// <exception cref="EndOfStreamException">There is not enough space to write all the bytes.</exception>
            /// <exception cref="ArgumentNullException">The argument <paramref name="buffer"/> is <see langword="null"/>.</exception>
            /// <exception cref="IOException">An I/O error occurred.</exception>
            [DebuggerStepThrough]
            void IVolume.Write(long offset, byte[] buffer)
            {
                //
            }

            /// <summary>
            /// Writes the contents of <paramref name="buffer"/> to the volume.
            /// </summary>
            /// <param name="offset">The byte offset to start writing at.</param>
            /// <param name="buffer">The array containing the bytes to write.</param>
            /// <param name="index">The index into <paramref name="buffer"/> to copy the bytes from.</param>
            /// <param name="count">The count of bytes to copy.</param>
            /// <exception cref="EndOfStreamException">There is not enough space to write all the bytes.</exception>
            /// <exception cref="ArgumentNullException">The argument <paramref name="buffer"/> is <see langword="null"/>.</exception>
            /// <exception cref="IOException">An I/O error occurred.</exception>
            [DebuggerStepThrough]
            void IVolume.Write(long offset, byte[] buffer, int index, int count)
            {
                //
            }

            #endregion

            #region IDisposable Members

            /// <summary>
            /// Releases all resources used by this <see cref="NullVolume"/> instance.
            /// </summary>
            [DebuggerStepThrough]
            void IDisposable.Dispose()
            {
                //
            }

            #endregion
        }

        /// <summary>
        /// Implements a dummy index area entry recycle callback.
        /// </summary>
        private class NullRecycleCallback : Object, IEntryRecycleCallback
        {
            #region Constructors
            /// <overloads>Initializes a new instance of <see cref="NullRecycleCallback"/>.</overloads>
            /// <summary>
            /// Initializes a new instance of <see cref="NullRecycleCallback"/>.
            /// </summary>
            [DebuggerStepThrough]
            public NullRecycleCallback()
                : base()
            {
            }
            #endregion

            #region IEntryRecycleCallback Members

            /// <summary>
            /// Called when the deleted entry is recycled.
            /// </summary>
            void IEntryRecycleCallback.OnRecycled()
            {
                //
            }

            #endregion
        }

        /// <summary>
        /// Implements a dummy index area entry move callback.
        /// </summary>
        private class NullMoveCallback : Object, IEntryMoveCallback
        {
            #region Constructors
            /// <overloads>Initializes a new instance of <see cref="NullMoveCallback"/>.</overloads>
            /// <summary>
            /// Initializes a new instance of <see cref="NullMoveCallback"/>.
            /// </summary>
            [DebuggerStepThrough]
            public NullMoveCallback()
                : base()
            {
            }
            #endregion

            #region IEntryMoveCallback Members

            /// <summary>
            /// Called when the entry is relocated.
            /// </summary>
            /// <param name="prev">The previous index area entry index.</param>
            /// <param name="current">The current index area entry index.</param>
            [DebuggerStepThrough]
            void IEntryMoveCallback.OnRelocated(int prev, int current)
            {
                //
            }

            #endregion
        }

        #endregion
    }
}
